Vue 2:响应式原理、data 设计、丢失响应式与 props
一、为何组件里 data 必须是函数
现象
- 根实例的
data可以是对象。 - 组件的
data必须是返回对象的函数,否则运行时会报错。
原因:避免状态共享
组件会被多次实例化(列表、重复标签)。若 data 是同一个对象引用,所有实例共享这份数据,一改全改。
写成 data() { return { ... } } 时,每次创建实例都会执行函数并得到新对象,实例间数据隔离。
与 export、引用的关系
- 模块里
export普通对象:多处 import 可能共享同一引用。 - 函数每次返回新对象字面量:引用不同,状态不串联。
- Vue 3 Composable 同理:函数内创建的状态每次调用独立。
表述纠正
旧稿问答中有「钓鱼这个函数」应为笔误,语义指 调用函数返回新对象。
二、响应式原理精讲(保留结构与代码骨架)
简述
初始化时递归遍历 data,用 Object.defineProperty 把属性改成访问器;读时 getter 做依赖收集,写时 setter 通知 Dep → Watcher 更新视图。
阶段归纳
- 初始化 observe:遍历
data,为属性建立 getter/setter,并为属性挂 Dep。 - 首次渲染 / Watcher:渲染函数读取数据 → 触发 getter → 把当前 Watcher 记到对应 Dep。
- 修改数据:setter →
dep.notify()→ Watcher 异步队列批量刷新(与$nextTick机制衔接)。 - 再次渲染:新 VNode 与旧 VNode patch(详见虚拟 DOM 章节)。
代码示例
文档中原有一段简化 Dep / Watcher / defineReactive / observe 示例,用于理解 getter/setter 与依赖队列;其核心与 Vue 真实实现一致的是「访问收集、修改通知」,队列调度与对象嵌套处理真实源码会更复杂。
局限性(再次归纳)
- 监听不到 新增/删除属性(需
$set/$delete或整体替换)。 - 数组下标直接赋值等场景需借助封装过的数组方法或
$set。
getter/setter 命名
Object.defineProperty 的配置项固定为 get / set,不能随意改名;这与 ES 访问器属性语义一致。
三、丢失响应式:场景与处理
- 对象新增属性:
this.$set(obj, key, val)或this.obj = Object.assign({}, this.obj, { ... })。 - 对象删除属性:
this.$delete(obj, key)。 - 数组按下标改:
this.$set(arr, i, val)或splice。 - 改数组长度:如
splice清空等。 - 未在
data声明就使用:应先在data里声明字段。
四、获取 data 初始快照
const initialData = this.$options.data.call(this)- 或在模块级保存常量模板,
data()里JSON.parse(JSON.stringify(originalData))(注意结构化克隆局限)。
五、父改 props 子组件「不更新」?
场景特征
常为 对象 props,只改内部字段而未替换引用;Vue 2 对 props 的检测方式可能导致子组件侧未按预期触发更新。
处理思路
- 父:
this.$set(this.obj, 'key', val),或展开运算符生成新对象引用赋回。 - 子:用 computed 派生展示值;或对 props deep watch。
说明
Vue 3 中 props 与响应式实现不同,此类现象较少以同样形式出现,但仍应遵循单向数据流。
六、能否在子组件里改 props?
不推荐直接改 props,违背单向数据流;若必须「本地可编辑副本」:
- 拷贝到 data 作初始值(与父后续变更是否同步需自行
watch)。 computed+ getter/setter,setter 里$emit('input', val)(Vue 2)实现受控组件。- 引用类型 props:改嵌套属性技术上可能影响父对象,但不利于追溯,团队规范通常禁止。
文档中 Vue 3 defineProps 片段属跨版本示例,若仍在 Vue 2 项目请用选项式 API 写法。
七、小结
- 响应式:
Object.defineProperty+ Dep/Watcher + 异步更新队列。 - data 函数:保证多实例隔离。
- 边界:增删属性、数组下标、
props深层变更 —— 用 API 或模式规避。
下一章:模板与指令。
